/*
 * Copyright (c) 2013 Hai Bison
 *
 * See the file LICENSE at the root directory of this project for copying
 * permission.
 */

package com.haibison.android.uyenkha;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;

import android.app.Activity;
import android.app.IntentService;
import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ServiceInfo;
import android.content.pm.Signature;
import android.os.Parcelable;
import android.os.Process;
import android.text.TextUtils;

/**
 * The pirate service.
 * <p/>
 * You start this service with some extras to have it check your's app of pirate
 * uses.
 * <p/>
 * <ul>
 * <li>See {@link #EXTRA_EXECUTOR}.</li>
 * <li>About {@link #EXTRA_FINGERPRINTS}: it should hold an array list of
 * {@code char} arrays. Every {@code char} array is an SHA-256 fingerprint of
 * the Java keystore you allow to use. To obtain an SHA-256 fingerprint of your
 * keystore, use the Java tool <i>keytool</i> with this command line:
 * 
 * <pre>
 * <code>keytool -list -v -keystore [path/to/your/keystore]</code>
 * </pre>
 * 
 * Or have a look at open source Swing tool <a
 * href="https://code.google.com/p/apk-signer">apk-signer</a>.
 * 
 * </li>
 * </ul>
 * <p/>
 * There are some helper methods which help you reduce the code, check out
 * {@link #newIntent(Context, Parcelable, ArrayList, boolean)},
 * {@link #newIntent(Context, Parcelable, char[][], boolean)}, or even better
 * with {@link #start(Context, Parcelable, ArrayList, boolean)} /
 * {@link #start(Context, Parcelable, char[][], boolean)}.
 * 
 * @author Hai Bison
 * @since v1.0 beta
 */
public class PirateService extends IntentService {

    /**
     * Used for debugging...
     */
    private static final String CLASSNAME = PirateService.class.getName();

    /**
     * The unique ID of this library. (This was generated by Python ^^)
     */
    public static final String UUID = "9bb14fb9-a92d-497e-8147-7cc41bdd8305";

    /**
     * The library name.
     */
    public static final String LIB_NAME = "Uyên Kha";

    /**
     * The library code name.
     */
    public static final String LIB_CODE_NAME = "uyen-kha";

    /**
     * The library version code.
     */
    public static final int LIB_VERSION_CODE = 19;

    /**
     * The library version name.
     */
    public static final String LIB_VERSION_NAME = "2.1.3";

    /**
     * If this service finds out that the app is in pirate mode, it'll call your
     * intent with this action -- <b><i>if</i></b> your original intent doesn't
     * use one.
     */
    public static final String ACTION_SAY_HI_TO_PIRATE = CLASSNAME
            + ".say_hi_to_pirate";

    /**
     * You put an array list of char arrays into this extra -- which means
     * {@code ArrayList<char[]>}. It contains your app's <b>legal</b>
     * fingerprint(s). They will be used to compare to current app's
     * fingerprint.
     * <p/>
     * For your convenience, you can use helper method
     * {@link #toSerializableList(char[][])}. Also, this extra is optional but
     * this service will <b>not</b> work if you don't provide it.
     */
    public static final String EXTRA_FINGERPRINTS = CLASSNAME + ".fingerprints";

    /**
     * You can put an intent of an activity or a service or a
     * {@link PendingIntent} into this extra. It will be called when pirate use
     * is detected.
     * <p/>
     * <h1>Notes</h1>
     * <p/>
     * <ul>
     * <li>If you're going to use an intent of an activity, you <i>don't</i>
     * need to add this flag {@link Intent#FLAG_ACTIVITY_NEW_TASK}. Because it
     * will be started outside of your session, so this service will
     * automatically add that flag for your convenience. But if you want to use
     * a pending intent of an activity, you must explicitly set that flag.</li>
     * <li>In any cases, if you don't explicitly set any action, the action of
     * original intent will be set to {@link #ACTION_SAY_HI_TO_PIRATE}.</li>
     * <li>This extra is optional.</li>
     * </ul>
     */
    public static final String EXTRA_EXECUTOR = CLASSNAME + ".executor";

    /**
     * If set to {@code true} and the service finds out some "pirate" signature,
     * it will kill the main process by {@link Process#killProcess(int)}.
     * Default is {@code false}.
     * <p/>
     * Of course, if {@code true}, {@link #EXTRA_EXECUTOR} will be ignored.
     */
    public static final String EXTRA_KILL_SELF = CLASSNAME + ".kill_self";

    /**
     * Converts a 2-dimensional array to a serializable list, which can be put
     * into an {@link Intent} for example.
     * 
     * @param input
     *            an array of array of {@code char}.
     * @return an array list of array of {@code char}.
     */
    public static ArrayList<char[]> toSerializableList(char[][] input) {
        ArrayList<char[]> result = new ArrayList<char[]>();

        for (char[] chars : input)
            result.add(chars);

        return result;
    }// toSerializableList()

    /**
     * Creates new intent which can be used to start this service.
     * 
     * @param context
     *            the context.
     * @param executor
     *            see {@link #EXTRA_EXECUTOR}.
     * @param fingerprints
     *            see {@link #EXTRA_FINGERPRINTS}.
     * @param killSelf
     *            see {@link #EXTRA_KILL_SELF}.
     * @return the intent of this service.
     */
    public static Intent newIntent(Context context, Parcelable executor,
            char[][] fingerprints, boolean killSelf) {
        return newIntent(context, executor, toSerializableList(fingerprints),
                killSelf);
    }// newIntent()

    /**
     * Creates new intent which can be used to start this service.
     * 
     * @param context
     *            the context.
     * @param executor
     *            see {@link #EXTRA_EXECUTOR}.
     * @param fingerprints
     *            see {@link #EXTRA_FINGERPRINTS}.
     * @param killSelf
     *            see {@link #EXTRA_KILL_SELF}.
     * @return the intent of this service.
     */
    public static Intent newIntent(Context context, Parcelable executor,
            ArrayList<char[]> fingerprints, boolean killSelf) {
        checkState(context, killSelf);

        Intent result = new Intent(context, PirateService.class);
        result.putExtra(EXTRA_EXECUTOR, executor);
        result.putExtra(EXTRA_FINGERPRINTS, fingerprints);
        result.putExtra(EXTRA_KILL_SELF, killSelf);

        return result;
    }// newIntent()

    /**
     * Checks to see if this service is declared and enabled. If not, sets
     * pirate mode to {@code true}, and kills this process if {@code killSelf}
     * is {@code true}.
     * 
     * @param context
     *            the context.
     * @param killSelf
     *            see {@link #EXTRA_KILL_SELF}.
     */
    private static void checkState(Context context, boolean killSelf) {
        try {
            ServiceInfo serviceInfo = context.getPackageManager()
                    .getServiceInfo(
                            new ComponentName(context, PirateService.class), 0);
            if (!serviceInfo.enabled)
                throw new Exception();
        } catch (Exception e) {
            Settings.setPirateMode(context, true);
            if (killSelf)
                Process.killProcess(Process.myPid());
        }
    }// checkState()

    /**
     * Starts this service.
     * 
     * @param context
     *            the context.
     * @param executor
     *            see {@link #EXTRA_EXECUTOR}.
     * @param fingerprints
     *            see {@link #EXTRA_FINGERPRINTS}.
     * @param killSelf
     *            see {@link #EXTRA_KILL_SELF}.
     */
    public static void start(Context context, Parcelable executor,
            char[][] fingerprints, boolean killSelf) {
        start(context, executor, toSerializableList(fingerprints), killSelf);
    }// start()

    /**
     * Starts this service.
     * 
     * @param context
     *            the context.
     * @param executor
     *            see {@link #EXTRA_EXECUTOR}.
     * @param fingerprints
     *            see {@link #EXTRA_FINGERPRINTS}.
     * @param killSelf
     *            see {@link #EXTRA_KILL_SELF}.
     */
    public static void start(Context context, Parcelable executor,
            ArrayList<char[]> fingerprints, boolean killSelf) {
        context.startService(newIntent(context, executor, fingerprints,
                killSelf));
    }// start()

    /**
     * Creates new instance.
     */
    public PirateService() {
        super(CLASSNAME);
    }// PirateService()

    @Override
    protected void onHandleIntent(Intent intent) {
        @SuppressWarnings("unchecked")
        final ArrayList<char[]> legal_fingerprints = (ArrayList<char[]>) intent
                .getSerializableExtra(EXTRA_FINGERPRINTS);
        if (legal_fingerprints == null)
            return;

        final boolean killSelf = intent.getBooleanExtra(EXTRA_KILL_SELF, false);
        final Parcelable parcelable = intent.getParcelableExtra(EXTRA_EXECUTOR);

        final PackageInfo packageInfo;
        try {
            packageInfo = getPackageManager().getPackageInfo(getPackageName(),
                    PackageManager.GET_SIGNATURES);
        } catch (NameNotFoundException e) {
            /*
             * We should never be here. But in that case, just do nothing.
             */
            return;
        }

        /*
         * Get all signatures.
         */
        Signature[] signatures = packageInfo.signatures;
        if (signatures == null || signatures.length <= 0)
            return;

        /*
         * Now find the "pirate" signature.
         */
        for (int i = 0; i < signatures.length; i++) {
            final char[] app_fingerprint = calcSha256Fingerprint(signatures[i]
                    .toByteArray());
            for (final char[] _ : legal_fingerprints) {
                if (Arrays.equals(app_fingerprint, _)) {
                    Settings.setPirateMode(this, false);
                    return;
                }
            }
        }// for

        /*
         * GOT IT!
         */
        Settings.setPirateMode(this, true);

        if (killSelf) {
            Process.killProcess(Process.myPid());
            /*
             * Not sure if `return` is necessary ^^
             */
            return;
        }

        if (parcelable instanceof Intent) {
            if (TextUtils.isEmpty(((Intent) parcelable).getAction()))
                ((Intent) parcelable).setAction(ACTION_SAY_HI_TO_PIRATE);

            Class<?> clz;
            try {
                clz = Class.forName(((Intent) parcelable).getComponent()
                        .getClassName());
            } catch (ClassNotFoundException e) {
                /*
                 * Ignore it.
                 */
                return;
            }

            if (clz == null)
                return;

            if (Activity.class.isAssignableFrom(clz)) {
                ((Intent) parcelable).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                startActivity((Intent) parcelable);
            } else if (Service.class.isAssignableFrom(clz))
                startService((Intent) parcelable);
        }// executor is an Intent
        else if (parcelable instanceof PendingIntent) {
            try {
                ((PendingIntent) parcelable).send(this, 0, new Intent(
                        ACTION_SAY_HI_TO_PIRATE));
            } catch (CanceledException e) {
                /*
                 * Ignore it.
                 */
            }
        }// executor is a PendingIntent
    }// onHandleIntent()

    /**
     * Calculates SHA-256 of an array of bytes, then prints it as a digital
     * fingerprint form (<i>lower case</i>). For example:
     * <p/>
     * 
     * <pre>
     * <code>00:99:aa:ff:...</code>
     * </pre>
     * 
     * @param bytes
     *            the source data.
     * @return the SHA-256 fingerprint of {@code bytes}.
     */
    public static char[] calcSha256Fingerprint(byte[] bytes) {
        bytes = SimpleWeakEncryption.sha256(bytes);
        BigInteger bi = new BigInteger(1, bytes);
        StringBuilder sb = new StringBuilder(String.format("%0"
                + (bytes.length * 2) + "x", bi));
        final int count = sb.length() / 2 - 1;
        for (int i = 1; i <= count; i++)
            sb.insert(3 * i - 1, ':');

        return sb.toString().toCharArray();
    }// calcSha256Fingerprint()

}
